package org.dromara.common.log.aspect;

import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessStatus;
import org.dromara.common.log.event.OperLogEvent;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：操作日志记录处理
 */
@Slf4j
@Aspect
@AutoConfiguration
public class LogAspect {

    /**
     * 排除敏感属性字段
     */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };


    /**
     * 计时 key
     */
    private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();

    /**
     * 处理请求前执行
     * 在带有controllerLog注解的方法执行之前进行操作
     * 该方法主要用于开始计算方法的执行时间，并记录到缓存中
     *
     * @param joinPoint 切入点对象，用于获取目标方法的签名等信息
     * @param controllerLog 控制器日志对象，通过注解获取
     */
    @Before(value = "@annotation(controllerLog)")
    public void doBefore(JoinPoint joinPoint, Log controllerLog) {
        // 创建一个StopWatch对象用于计算方法的执行时间
        StopWatch stopWatch = new StopWatch();
        // 将StopWatch对象设置到缓存中，键为KEY_CACHE
        KEY_CACHE.set(stopWatch);
        // 开始计算时间
        stopWatch.start();
    }

    /**
     * 处理完请求后执行
     * 在方法执行完成后处理日志
     * 该方法用@AfterReturning注解标记，作为一个后置通知，用于捕获被Spring AOP拦截的方法执行返回后的操作
     * 主要用于记录控制器方法的执行结果，如操作成功或失败等信息
     *
     * @param joinPoint 切点
     * @param controllerLog 日志注解对象，包含日志记录的相关信息，如日志的描述、作者等
     * @param jsonResult 方法的返回结果对象，可以是任何类型，此处用于记录或传递方法的执行结果
     */
    // 定义一个切面，用于在方法执行完成后进行操作
    // 该切面的目标是拦截被 ControllerLog 注解标记的方法
    // 并将方法的返回值以 JSON 格式进行封装
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        // 处理日志
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    // 定义一个方法，在执行被指定的注解标记的方法后，如果抛出了异常，则执行此方法
    // 该方法主要用于捕获并处理方法执行过程中可能抛出的异常，以便进行日志记录或者其他处理
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        // 处理日志
        handleLog(joinPoint, controllerLog, e, null);
    }

    /**
     * 处理日志
     *
     * @param joinPoint 切点对象，用于获取方法信息
     * @param controllerLog 控制器日志对象，用于获取操作描述等信息
     * @param e 异常对象，用于记录方法执行过程中的异常
     * @param jsonResult 返回的JSON结果对象，用于记录方法返回值
     */
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {

            // *========数据库日志=========*//
            OperLogEvent operLog = new OperLogEvent();
            // 设置租户ID
            operLog.setTenantId(LoginHelper.getTenantId());
            // 默认设置操作状态为成功
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = ServletUtils.getClientIP();
            operLog.setOperIp(ip);
            // 请求的URL
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
            // 获取当前登录用户信息
            LoginUser loginUser = LoginHelper.getLoginUser();
            operLog.setOperName(loginUser.getUsername());
            operLog.setDeptName(loginUser.getDeptName());

            if (e != null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 设置消耗时间
            StopWatch stopWatch = KEY_CACHE.get();
            stopWatch.stop();
            operLog.setCostTime(stopWatch.getTime());
            // 发布事件保存数据库
            SpringUtils.context().publishEvent(operLog);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        } finally {
            // 移除线程本地缓存中的StopWatch对象
            KEY_CACHE.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param joinPoint 切点对象，用于获取方法执行的动态信息
     * @param log     日志，方法上的Log注解对象，包含操作日志信息
     * @param operLog 操作日志，用于记录操作日志的详细信息
     * @throws Exception 执行过程中可能抛出的异常
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request，参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息，传入到数据库中。
            setRequestValue(joinPoint, operLog, log.excludeParamNames());
        }
        // 是否需要保存response，参数和值
        if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
            // 记录方法返回的json结果，限制长度为2000字符
            operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数，放到log中
     * 此方法的目的是将HTTP请求的参数记录到操作日志中，以便于后续的日志审计和问题追踪
     * 它主要处理的是POST和PUT类型的请求，对于这两种请求类型，会分别处理参数
     * 如果请求参数为空且请求方法是PUT或POST，则将joinPoint中的参数转为字符串并记录
     * 否则，会将请求中的参数过滤掉不需要记录的参数后，转为JSON字符串并记录
     *
     * @param joinPoint 切点对象，用于获取方法参数等信息
     * @param operLog 操作日志，用于记录操作日志
     * @param excludeParamNames 排除的参数名称数组，这些参数不会被记录到操作日志中
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
        // 获取请求参数
        Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        // 获取请求方法（GET、POST等）
        String requestMethod = operLog.getRequestMethod();
        // 如果请求参数为空且请求方法为PUT或POST
        if (MapUtil.isEmpty(paramsMap)
            && HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
            // 将方法参数转为字符串并记录到操作日志中
            String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            // 过滤掉不需要记录的参数
            MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
            MapUtil.removeAny(paramsMap, excludeParamNames);
            // 将过滤后的参数转为JSON字符串并记录到操作日志中
            operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
        }
    }

    /**
     * 参数拼装
     * 将参数数组转换为字符串
     *
     * 此方法主要用于将传递给方法的参数数组转换为单个字符串表示
     * 它处理了参数为空的情况，并且可以通过排除参数名称来忽略某些参数
     *
     * @param paramsArray 方法参数数组
     * @param excludeParamNames 要排除的参数名称数组
     * @return 参数的字符串表示，参数之间用空格分隔
     */
    private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
        StringJoiner params = new StringJoiner(" ");
        if (ArrayUtil.isEmpty(paramsArray)) {
            return params.toString();
        }
        for (Object o : paramsArray) {
            // 如果参数非空且不被过滤，则进行处理
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
                // 将对象转换为JSON字符串
                String str = JsonUtils.toJsonString(o);
                // 将JSON字符串解析为字典对象
                Dict dict = JsonUtils.parseMap(str);

                // 如果字典非空，则进行处理
                if (MapUtil.isNotEmpty(dict)) {
                    // 移除任何不希望包含在最终字符串中的属性
                    MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
                    MapUtil.removeAny(dict, excludeParamNames);
                    // 将处理后的字典转换回JSON字符串
                    str = JsonUtils.toJsonString(dict);
                }
                params.add(str);
            }
        }
        return params.toString();
    }

    /**
     * 判断是否需要过滤的对象。
     * 判断对象是否为需要过滤的类型
     * <p>
     * 本方法旨在判断传入的对象是否为需要特殊处理的类型，主要是为了在处理请求参数时进行类型过滤<br>
     * 需要过滤的对象类型包括：MultipartFile（文件上传类型）、HttpServletRequest（请求对象）、
     * HttpServletResponse（响应对象）、BindingResult（绑定结果对象）
     * </p>
     *
     * @param o 对象信息。待判断的对象
     * @return 如果是需要过滤的对象，则返回true；否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        // 获取对象的类信息
        Class<?> clazz = o.getClass();

        // 检查对象是否为数组类型
        if (clazz.isArray()) {
            // 获取数组元素的类型，并判断是否为MultipartFile类型或其子类
            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
        }
        // 检查对象是否为Collection类型或其子类
        else if (Collection.class.isAssignableFrom(clazz)) {
            // 将对象转换为Collection类型，并遍历其元素
            Collection collection = (Collection) o;
            for (Object value : collection) {
                // 如果集合中任一元素为MultipartFile类型，则返回true
                if (value instanceof MultipartFile) {
                    return true;
                }
            }
        }
        // 检查对象是否为Map类型或其子类
        else if (Map.class.isAssignableFrom(clazz)) {
            // 将对象转换为Map类型，并遍历其值
            Map map = (Map) o;
            for (Object value : map.values()) {
                // 如果Map中任一值为MultipartFile类型，则返回true
                if (value instanceof MultipartFile) {
                    return true;
                }
            }
        }

        // 检查对象是否为需要过滤的类型之一
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
            || o instanceof BindingResult;
    }
}
